/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.vove7.energy_ring.ui.view.slider;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Style;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.Region.Op;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.SeekBar;

import androidx.annotation.ColorInt;
import androidx.annotation.DimenRes;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.RangeInfoCompat;
import androidx.customview.widget.ExploreByTouchHelper;

import com.google.android.material.R;
import com.google.android.material.drawable.DrawableUtils;
import com.google.android.material.internal.DescendantOffsetUtils;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.ViewOverlayImpl;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.android.material.slider.BaseOnChangeListener;
import com.google.android.material.slider.BaseOnSliderTouchListener;
import com.google.android.material.slider.BasicLabelFormatter;
import com.google.android.material.slider.LabelFormatter;
import com.google.android.material.tooltip.TooltipDrawable;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import static androidx.core.math.MathUtils.clamp;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_FLOAT;
import static com.google.android.material.slider.LabelFormatter.LABEL_FLOATING;
import static com.google.android.material.slider.LabelFormatter.LABEL_GONE;
import static com.google.android.material.slider.LabelFormatter.LABEL_WITHIN_BOUNDS;
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
import static java.lang.Float.compare;
import static java.lang.Math.abs;

/**
 * The slider can function either as a continuous slider, or as a discrete slider. The mode of
 * operation is controlled by the value of the step size. If the step size is set to 0, the slider
 * operates as a continuous slider where the slider's thumb can be moved to any position along the
 * horizontal line. If the step size is set to a number greater than 0, the slider operates as a
 * discrete slider where the slider's thumb will snap to the closest valid value. See {@link
 * #setStepSize(float)}.
 *
 * <p>The {@link LabelFormatter} interface defines a formatter to be used to render text within the
 * value indicator label on interaction.
 *
 * <p>{@link BasicLabelFormatter} is a simple implementation of the {@link LabelFormatter} that
 * displays the selected value using letters to indicate magnitude (e.g.: 1.5K, 3M, 12B, etc..).
 *
 * <p>With the default style {@link
 * com.google.android.material.R.style#Widget_MaterialComponents_Slider}, colorPrimary and
 * colorOnPrimary are used to customize the color of the slider when enabled, and colorOnSurface is
 * used when disabled. The following attributes are used to customize the slider's appearance
 * further:
 *
 * <ul>
 *   <li>{@code haloColor}: the color of the halo around the thumb.
 *   <li>{@code haloRadius}: The radius of the halo around the thumb.
 *   <li>{@code labelBehavior}: The behavior of the label which can be {@code LABEL_FLOATING},
 *       {@code LABEL_WITHIN_BOUNDS}, or {@code LABEL_GONE}. See {@link LabelBehavior} for more
 *       information.
 *   <li>{@code labelStyle}: the style to apply to the value indicator {@link TooltipDrawable}.
 *   <li>{@code thumbColor}: the color of the slider's thumb.
 *   <li>{@code thumbElevation}: the elevation of the slider's thumb.
 *   <li>{@code thumbRadius}: The radius of the slider's thumb.
 *   <li>{@code tickColorActive}: the color of the slider's tick marks for the active part of the
 *       track. Only used when the slider is in discrete mode.
 *   <li>{@code tickColorInactive}: the color of the slider's tick marks for the inactive part of
 *       the track. Only used when the slider is in discrete mode.
 *   <li>{@code tickColor}: the color of the slider's tick marks. Only used when the slider is in
 *       discrete mode. This is a short hand for setting both the {@code tickColorActive} and {@code
 *       tickColorInactive} to the same thing. This takes precedence over {@code tickColorActive}
 *       and {@code tickColorInactive}.
 *   <li>{@code trackColorActive}: The color of the active part of the track.
 *   <li>{@code trackColorInactive}: The color of the inactive part of the track.
 *   <li>{@code trackColor}: The color of the whole track. This is a short hand for setting both the
 *       {@code trackColorActive} and {@code trackColorInactive} to the same thing. This takes
 *       precedence over {@code trackColorActive} and {@code trackColorInactive}.
 *   <li>{@code trackHeight}: The height of the track.
 * </ul>
 *
 * <p>The following XML attributes are used to set the slider's various parameters of operation:
 *
 * <ul>
 *   <li>{@code android:valueFrom}: <b>Required.</b> The slider's minimum value. This attribute must
 *       be less than {@code valueTo} or an {@link IllegalStateException} will be thrown when the
 *       view is laid out.
 *   <li>{@code android:valueTo}: <b>Required.</b> The slider's maximum value. This attribute must
 *       be greater than {@code valueFrom} or an {@link IllegalStateException} will be thrown when
 *       the view is laid out.
 *   <li>{@code android:value}: <b>Optional.</b> The initial value of the slider. If not specified,
 *       the slider's minimum value {@code android:valueFrom} is used.
 *   <li>{@code android:stepSize}: <b>Optional.</b> This value dictates whether the slider operates
 *       in continuous mode, or in discrete mode. If missing or equal to 0, the slider operates in
 *       continuous mode. If greater than 0 and evenly divides the range described by {@code
 *       valueFrom} and {@code valueTo}, the slider operates in discrete mode. If negative an {@link
 *       IllegalArgumentException} is thrown, or if greater than 0 but not a factor of the range
 *       described by {@code valueFrom} and {@code valueTo}, an {@link IllegalStateException} will
 *       be thrown when the view is laid out.
 * </ul>
 *
 * @attr ref com.google.android.material.R.styleable#Slider_android_stepSize
 * @attr ref com.google.android.material.R.styleable#Slider_android_valueFrom
 * @attr ref com.google.android.material.R.styleable#Slider_android_valueTo
 * @attr ref com.google.android.material.R.styleable#Slider_haloColor
 * @attr ref com.google.android.material.R.styleable#Slider_haloRadius
 * @attr ref com.google.android.material.R.styleable#Slider_labelBehavior
 * @attr ref com.google.android.material.R.styleable#Slider_labelStyle
 * @attr ref com.google.android.material.R.styleable#Slider_thumbColor
 * @attr ref com.google.android.material.R.styleable#Slider_thumbElevation
 * @attr ref com.google.android.material.R.styleable#Slider_thumbRadius
 * @attr ref com.google.android.material.R.styleable#Slider_tickColor
 * @attr ref com.google.android.material.R.styleable#Slider_tickColorActive
 * @attr ref com.google.android.material.R.styleable#Slider_tickColorInactive
 * @attr ref com.google.android.material.R.styleable#Slider_trackColor
 * @attr ref com.google.android.material.R.styleable#Slider_trackColorActive
 * @attr ref com.google.android.material.R.styleable#Slider_trackColorInactive
 * @attr ref com.google.android.material.R.styleable#Slider_trackHeight
 */
@SuppressLint({"RestrictedApi", "PrivateResource"})
abstract class BaseSlider<
        S extends BaseSlider<S, L, T>,
        L extends BaseOnChangeListener<S>,
        T extends BaseOnSliderTouchListener<S>>
        extends View {

    private static final String TAG = BaseSlider.class.getSimpleName();
    private static final String EXCEPTION_ILLEGAL_VALUE =
            "Slider value(%s) must be greater or equal to valueFrom(%s), and lower or equal to"
                    + " valueTo(%s)";
    private static final String EXCEPTION_ILLEGAL_DISCRETE_VALUE =
            "Value(%s) must be equal to valueFrom(%s) plus a multiple of stepSize(%s) when using"
                    + " stepSize(%s)";
    private static final String EXCEPTION_ILLEGAL_VALUE_FROM =
            "valueFrom(%s) must be smaller than valueTo(%s)";
    private static final String EXCEPTION_ILLEGAL_VALUE_TO =
            "valueTo(%s) must be greater than valueFrom(%s)";
    private static final String EXCEPTION_ILLEGAL_STEP_SIZE =
            "The stepSize(%s) must be 0, or a factor of the valueFrom(%s)-valueTo(%s) range";
    private static final String EXCEPTION_SUPPORT_REVERSE_SIZE_MUST_BE_2 =
            "The values size must be 2 if you set supportReverse to true";

    private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
    private static final int HALO_ALPHA = 63;
    private static final double THRESHOLD = .0001;

    private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_Slider;

    @NonNull
    private final Paint inactiveTrackPaint;
    @NonNull
    private final Paint activeTrackPaint;
    @NonNull
    private final Paint thumbPaint;
    @NonNull
    private final Paint haloPaint;
    @NonNull
    private final Paint inactiveTicksPaint;
    @NonNull
    private final Paint activeTicksPaint;
    @NonNull
    private final AccessibilityHelper accessibilityHelper;
    private final AccessibilityManager accessibilityManager;
    private AccessibilityEventSender accessibilityEventSender;

    private interface TooltipDrawableFactory {
        TooltipDrawable createTooltipDrawable();
    }

    @NonNull
    private final TooltipDrawableFactory labelMaker;
    @NonNull
    private final List<TooltipDrawable> labels = new ArrayList<>();
    @NonNull
    private final List<L> changeListeners = new ArrayList<>();
    @NonNull
    private final List<T> touchListeners = new ArrayList<>();

    private final int scaledTouchSlop;

    private int widgetHeight;
    private int labelBehavior;
    private int trackHeight;
    private int trackSidePadding;
    private int trackTop;
    private int thumbRadius;
    private int haloRadius;
    private int labelPadding;
    private float touchDownX;
    private MotionEvent lastEvent;
    private LabelFormatter formatter;
    private boolean thumbIsPressed = false;
    private float valueFrom;
    private float valueTo;
    private boolean supportReverse;

    // Holds the values set to this slider. We keep this array sorted in order to check if the value
    // has been changed when a new value is set and to find the minimum and maximum values.
    private ArrayList<Float> values = new ArrayList<>();
    // The index of the currently touched thumb.
    private int activeThumbIdx = -1;
    // The index of the currently focused thumb.
    private int focusedThumbIdx = -1;
    private float stepSize = 0.0f;
    private float[] ticksCoordinates;
    private int trackWidth;
    private boolean forceDrawCompatHalo;
    private boolean isLongPress = false;
    private boolean dirtyConfig;

    @NonNull
    private ColorStateList haloColor;
    @NonNull
    private ColorStateList tickColorActive;
    @NonNull
    private ColorStateList tickColorInactive;
    @NonNull
    private ColorStateList trackColorActive;
    @NonNull
    private ColorStateList trackColorInactive;

    @NonNull
    private final MaterialShapeDrawable thumbDrawable = new MaterialShapeDrawable();

    private float touchPosition;

    /**
     * Determines the behavior of the label which can be any of the following.
     *
     * <ul>
     *   <li>{@code LABEL_FLOATING}: The label will only be visible on interaction. It will float
     *       above the slider and may cover views above this one. This is the default and recommended
     *       behavior.
     *   <li>{@code LABEL_WITHIN_BOUNDS}: The label will only be visible on interaction. The label
     *       will always be drawn within the bounds of this view. This means extra space will be
     *       visible above the slider when the label is not visible.
     *   <li>{@code LABEL_GONE}: The label will never be drawn.
     * </ul>
     */
    @IntDef({LABEL_FLOATING, LABEL_WITHIN_BOUNDS, LABEL_GONE})
    @Retention(RetentionPolicy.SOURCE)
    @interface LabelBehavior {
    }

    public BaseSlider(@NonNull Context context) {
        this(context, null);
    }

    public BaseSlider(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.sliderStyle);
    }

    public BaseSlider(
            @NonNull Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
        super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
        // Ensure we are using the correctly themed context rather than the context that was passed in.
        context = getContext();

        inactiveTrackPaint = new Paint();
        inactiveTrackPaint.setStyle(Style.STROKE);
        inactiveTrackPaint.setStrokeCap(Cap.ROUND);

        activeTrackPaint = new Paint();
        activeTrackPaint.setStyle(Style.STROKE);
        activeTrackPaint.setStrokeCap(Cap.ROUND);

        thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        thumbPaint.setStyle(Style.FILL);
        thumbPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));

        haloPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        haloPaint.setStyle(Style.FILL);

        inactiveTicksPaint = new Paint();
        inactiveTicksPaint.setStyle(Style.STROKE);
        inactiveTicksPaint.setStrokeCap(Cap.ROUND);

        activeTicksPaint = new Paint();
        activeTicksPaint.setStyle(Style.STROKE);
        activeTicksPaint.setStrokeCap(Cap.ROUND);

        loadResources(context.getResources());

        // Because there's currently no way to copy the TooltipDrawable we use this to make more if more
        // thumbs are added.
        labelMaker =
                new TooltipDrawableFactory() {
                    @Override
                    public TooltipDrawable createTooltipDrawable() {
                        final TypedArray a =
                                ThemeEnforcement.obtainStyledAttributes(
                                        getContext(), attrs, R.styleable.Slider, defStyleAttr, DEF_STYLE_RES);
                        TooltipDrawable d = parseLabelDrawable(getContext(), a);
                        a.recycle();
                        return d;
                    }
                };

        processAttributes(context, attrs, defStyleAttr);

        setFocusable(true);
        setClickable(true);

        // Set up the thumb drawable to always show the compat shadow.
        thumbDrawable.setShadowCompatibilityMode(MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS);

        scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        accessibilityHelper = new AccessibilityHelper(this);
        ViewCompat.setAccessibilityDelegate(this, accessibilityHelper);

        accessibilityManager =
                (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
    }

    private void loadResources(@NonNull Resources resources) {
        widgetHeight = resources.getDimensionPixelSize(R.dimen.mtrl_slider_widget_height);

        trackSidePadding = resources.getDimensionPixelOffset(R.dimen.mtrl_slider_track_side_padding);
        trackTop = resources.getDimensionPixelOffset(R.dimen.mtrl_slider_track_top);

        labelPadding = resources.getDimensionPixelSize(R.dimen.mtrl_slider_label_padding);
    }


    private void processAttributes(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a =
                ThemeEnforcement.obtainStyledAttributes(
                        context, attrs, R.styleable.Slider, defStyleAttr, DEF_STYLE_RES);
        valueFrom = a.getFloat(R.styleable.Slider_android_valueFrom, 0.0f);
        valueTo = a.getFloat(R.styleable.Slider_android_valueTo, 1.0f);
        setValues(valueFrom);
        stepSize = a.getFloat(R.styleable.Slider_android_stepSize, 0.0f);

        boolean hasTrackColor = a.hasValue(R.styleable.Slider_trackColor);

        int trackColorInactiveRes =
                hasTrackColor ? R.styleable.Slider_trackColor : R.styleable.Slider_trackColorInactive;
        int trackColorActiveRes =
                hasTrackColor ? R.styleable.Slider_trackColor : R.styleable.Slider_trackColorActive;

        ColorStateList trackColorInactive =
                MaterialResources.getColorStateList(context, a, trackColorInactiveRes);
        setTrackInactiveTintList(
                trackColorInactive != null
                        ? trackColorInactive
                        : AppCompatResources.getColorStateList(
                        context, R.color.material_slider_inactive_track_color));
        ColorStateList trackColorActive =
                MaterialResources.getColorStateList(context, a, trackColorActiveRes);
        setTrackActiveTintList(
                trackColorActive != null
                        ? trackColorActive
                        : AppCompatResources.getColorStateList(
                        context, R.color.material_slider_active_track_color));
        ColorStateList thumbColor =
                MaterialResources.getColorStateList(context, a, R.styleable.Slider_thumbColor);
        thumbDrawable.setFillColor(thumbColor);
        ColorStateList haloColor =
                MaterialResources.getColorStateList(context, a, R.styleable.Slider_haloColor);
        setHaloTintList(
                haloColor != null
                        ? haloColor
                        : AppCompatResources.getColorStateList(context, R.color.material_slider_halo_color));

        boolean hasTickColor = a.hasValue(R.styleable.Slider_tickColor);
        int tickColorInactiveRes =
                hasTickColor ? R.styleable.Slider_tickColor : R.styleable.Slider_tickColorInactive;
        int tickColorActiveRes =
                hasTickColor ? R.styleable.Slider_tickColor : R.styleable.Slider_tickColorActive;
        ColorStateList tickColorInactive =
                MaterialResources.getColorStateList(context, a, tickColorInactiveRes);
        setTickInactiveTintList(
                tickColorInactive != null
                        ? tickColorInactive
                        : AppCompatResources.getColorStateList(
                        context, R.color.material_slider_inactive_tick_marks_color));
        ColorStateList tickColorActive =
                MaterialResources.getColorStateList(context, a, tickColorActiveRes);
        setTickActiveTintList(
                tickColorActive != null
                        ? tickColorActive
                        : AppCompatResources.getColorStateList(
                        context, R.color.material_slider_active_tick_marks_color));

        setThumbRadius(a.getDimensionPixelSize(R.styleable.Slider_thumbRadius, 0));
        setHaloRadius(a.getDimensionPixelSize(R.styleable.Slider_haloRadius, 0));

        setThumbElevation(a.getDimension(R.styleable.Slider_thumbElevation, 0));

        setTrackHeight(a.getDimensionPixelSize(R.styleable.Slider_trackHeight, 0));

        labelBehavior = a.getInt(R.styleable.Slider_labelBehavior, LABEL_FLOATING);
        a.recycle();
    }

    @NonNull
    private static TooltipDrawable parseLabelDrawable(
            @NonNull Context context, @NonNull TypedArray a) {
        return TooltipDrawable.createFromAttributes(
                context,
                null,
                0,
                a.getResourceId(R.styleable.Slider_labelStyle, R.style.Widget_MaterialComponents_Tooltip));
    }

    private void validateStepSize() {
        if (stepSize > 0.0f && ((valueTo - valueFrom) / stepSize) % 1 > THRESHOLD) {
            throw new IllegalStateException(
                    String.format(
                            EXCEPTION_ILLEGAL_STEP_SIZE,
                            Float.toString(stepSize),
                            Float.toString(valueFrom),
                            Float.toString(valueTo)));
        }
    }

    private void validateValues() {
        for (Float value : values) {
            if (value < valueFrom || value > valueTo) {
                throw new IllegalStateException(
                        String.format(
                                EXCEPTION_ILLEGAL_VALUE,
                                Float.toString(value),
                                Float.toString(valueFrom),
                                Float.toString(valueTo)));
            }
            if (stepSize > 0.0f && ((valueFrom - value) / stepSize) % 1 > THRESHOLD) {
                throw new IllegalStateException(
                        String.format(
                                EXCEPTION_ILLEGAL_DISCRETE_VALUE,
                                Float.toString(value),
                                Float.toString(valueFrom),
                                Float.toString(stepSize),
                                Float.toString(stepSize)));
            }
        }
    }

    private void validateSupportReverse() {
        if (supportReverse && getValues().size() != 2) {
            throw new IllegalStateException(EXCEPTION_ILLEGAL_DISCRETE_VALUE);
        }
    }

    private void validateConfigurationIfDirty() {
        if (dirtyConfig) {
            validateStepSize();
            validateValues();
            dirtyConfig = false;
        }
    }

    /**
     * Returns the slider's {@code valueFrom} value.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_android_valueFrom
     * @see #setValueFrom(float)
     */
    public float getValueFrom() {
        return valueFrom;
    }

    /**
     * Sets the slider's {@code valueFrom} value.
     *
     * <p>The {@code valueFrom} value must be strictly lower than the {@code valueTo} value. If that
     * is not the case, an {@link IllegalStateException} will be thrown when the view is laid out.
     *
     * @param valueFrom The minimum value for the slider's range of values
     * @attr ref com.google.android.material.R.styleable#Slider_android_valueFrom
     * @see #getValueFrom()
     */
    public void setValueFrom(float valueFrom) {
        this.valueFrom = valueFrom;
        dirtyConfig = true;
        postInvalidate();
    }

    /**
     * Returns the slider's {@code valueTo} value.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_android_valueTo
     * @see #setValueTo(float)
     */
    public float getValueTo() {
        return valueTo;
    }

    /**
     * Sets the slider's {@code valueTo} value.
     *
     * <p>The {@code valueTo} value must be strictly greater than the {@code valueFrom} value. If that
     * is not the case, an {@link IllegalStateException} will be thrown when the view is laid out.
     *
     * @param valueTo The maximum value for the slider's range of values
     * @attr ref com.google.android.material.R.styleable#Slider_android_valueTo
     * @see #getValueTo()
     */
    public void setValueTo(float valueTo) {
        this.valueTo = valueTo;
        dirtyConfig = true;
        postInvalidate();
    }

    @NonNull
    List<Float> getValues() {
        return new ArrayList<>(values);
    }

    /**
     * Sets multiple values for the slider. Each value will represent a different thumb.
     *
     * <p>Each value must be greater or equal to {@code valueFrom}, and lesser or equal to {@code
     * valueTo}. If that is not the case, an {@link IllegalStateException} will be thrown when the
     * view is laid out.
     *
     * <p>If the slider is in discrete mode (i.e. the tick increment value is greater than 0), the
     * values must be set to a value falls on a tick (i.e.: {@code value == valueFrom + x * stepSize},
     * where {@code x} is an integer equal to or greater than 0). If that is not the case, an {@link
     * IllegalStateException} will be thrown when the view is laid out.
     *
     * @param values An array of values to set.
     * @see #getValues()
     */
    void setValues(@NonNull Float... values) {
        ArrayList<Float> list = new ArrayList<>();
        Collections.addAll(list, values);
        setValuesInternal(list);
    }

    /**
     * Sets multiple values for the slider. Each value will represent a different thumb.
     *
     * <p>Each value must be greater or equal to {@code valueFrom}, and lesser or equal to {@code
     * valueTo}. If that is not the case, an {@link IllegalStateException} will be thrown when the
     * view is laid out.
     *
     * <p>If the slider is in discrete mode (i.e. the tick increment value is greater than 0), the
     * values must be set to a value falls on a tick (i.e.: {@code value == valueFrom + x * stepSize},
     * where {@code x} is an integer equal to or greater than 0). If that is not the case, an {@link
     * IllegalStateException} will be thrown when the view is laid out.
     *
     * @param values An array of values to set.
     * @throws IllegalArgumentException If {@code values} is empty.
     */
    void setValues(@NonNull List<Float> values) {
        setValuesInternal(new ArrayList<>(values));
    }

    /**
     * This method assumes the list passed in is a copy. It is split out so we can call it from {@link
     * #setValues(Float...)} and {@link #setValues(List)}
     */
    private void setValuesInternal(@NonNull ArrayList<Float> values) {
        if (values.isEmpty()) {
            throw new IllegalArgumentException("At least one value must be set");
        }

        if (!supportReverse) {
            Collections.sort(values);
        }

        if (this.values.size() == values.size()) {
            if (this.values.equals(values)) {
                return;
            }
        }

        this.values = values;
        dirtyConfig = true;
        // Only update the focused thumb index. The active thumb index will be updated on touch.
        focusedThumbIdx = 0;
        updateHaloHotspot();
        createLabelPool();
        dispatchOnChangedProgramatically();
        postInvalidate();
    }

    private void createLabelPool() {
        // If there are too many labels, remove the extra ones from the end.
        if (labels.size() > values.size()) {
            List<TooltipDrawable> tooltipDrawables = labels.subList(values.size(), labels.size());
            for (TooltipDrawable label : tooltipDrawables) {
                if (ViewCompat.isAttachedToWindow(this)) {
                    detachLabelFromContentView(label);
                }
            }
            tooltipDrawables.clear();
        }

        // If there's not enough labels, add more.
        while (labels.size() < values.size()) {
            TooltipDrawable tooltipDrawable = labelMaker.createTooltipDrawable();
            labels.add(tooltipDrawable);
            if (ViewCompat.isAttachedToWindow(this)) {
                attachLabelToContentView(tooltipDrawable);
            }
        }

        // Add a stroke if there is more than one label for when they overlap.
        int strokeWidth = labels.size() == 1 ? 0 : 1;
        for (TooltipDrawable label : labels) {
            label.setStrokeWidth(strokeWidth);
        }
    }

    /**
     * Returns the step size used to mark the ticks.
     *
     * <p>A step size of 0 means that the slider is operating in continuous mode. A step size greater
     * than 0 means that the slider is operating in discrete mode.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_android_stepSize
     * @see #setStepSize(float)
     */
    public float getStepSize() {
        return stepSize;
    }

    /**
     * Sets the step size to use to mark the ticks.
     *
     * <p>Setting this value to 0 will make the slider operate in continuous mode. Setting this value
     * to a number greater than 0 will make the slider operate in discrete mode.
     *
     * <p>The step size must evenly divide the range described by the {@code valueFrom} and {@code
     * valueTo}, it must be a factor of the range. If the step size is not a factor of the range, an
     * {@link IllegalStateException} will be thrown when this view is laid out.
     *
     * <p>Setting this value to a negative value will result in an {@link IllegalArgumentException}.
     *
     * @param stepSize The interval value at which ticks must be drawn. Set to 0 to operate the slider
     *                 in continuous mode and not have any ticks.
     * @throws IllegalArgumentException If the step size is less than 0
     * @attr ref com.google.android.material.R.styleable#Slider_android_stepSize
     * @see #getStepSize()
     */
    public void setStepSize(float stepSize) {
        if (stepSize < 0.0f) {
            throw new IllegalArgumentException(
                    String.format(
                            EXCEPTION_ILLEGAL_STEP_SIZE,
                            Float.toString(stepSize),
                            Float.toString(valueFrom),
                            Float.toString(valueTo)));
        }
        if (this.stepSize != stepSize) {
            this.stepSize = stepSize;
            dirtyConfig = true;
            postInvalidate();
        }
    }

    /**
     * Returns the index of the currently focused thumb
     */
    public int getFocusedThumbIndex() {
        return focusedThumbIdx;
    }

    /**
     * Sets the index of the currently focused thumb
     */
    public void setFocusedThumbIndex(int index) {
        if (index < 0 || index >= values.size()) {
            throw new IllegalArgumentException("index out of range");
        }
        focusedThumbIdx = index;
        accessibilityHelper.requestKeyboardFocusForVirtualView(focusedThumbIdx);
        postInvalidate();
    }

    protected void setActiveThumbIndex(int index) {
        activeThumbIdx = index;
    }

    /**
     * Returns the index of the currently active thumb, or -1 if no thumb is active
     */
    public int getActiveThumbIndex() {
        return activeThumbIdx;
    }

    /**
     * Registers a callback to be invoked when the slider changes.
     *
     * @param listener The callback to run when the slider changes
     */
    public void addOnChangeListener(@Nullable L listener) {
        changeListeners.add(listener);
    }

    /**
     * Removes a callback for value changes from this slider.
     *
     * @param listener The callback that'll stop receive slider changes
     */
    public void removeOnChangeListener(@NonNull L listener) {
        changeListeners.remove(listener);
    }

    /**
     * Removes all instances of attached to this slider
     */
    public void clearOnChangeListeners() {
        changeListeners.clear();
    }

    /**
     * Registers a callback to be invoked when the slider touch event is being started or stopped
     *
     * @param listener The callback to run when the slider starts or stops being touched
     */
    public void addOnSliderTouchListener(@NonNull T listener) {
        touchListeners.add(listener);
    }

    /**
     * Removes a callback to be invoked when the slider touch event is being started or stopped
     *
     * @param listener The callback that'll stop be notified when the slider is being touched
     */
    public void removeOnSliderTouchListener(@NonNull T listener) {
        touchListeners.remove(listener);
    }

    /**
     * Removes all instances of touch listeners attached to this slider
     */
    public void clearOnSliderTouchListeners() {
        touchListeners.clear();
    }

    /**
     * Returns {@code true} if the slider has a {@link LabelFormatter} attached, {@code false}
     * otherwise.
     */
    public boolean hasLabelFormatter() {
        return formatter != null;
    }

    /**
     * Registers a {@link LabelFormatter} to be used to format the value displayed in the bubble shown
     * when the slider operates in discrete mode.
     *
     * @param formatter The {@link LabelFormatter} to use to format the bubble's text
     */
    public void setLabelFormatter(@Nullable LabelFormatter formatter) {
        this.formatter = formatter;
    }

    /**
     * Returns the elevation of the thumb.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbElevation
     * @see #setThumbElevation(float)
     * @see #setThumbElevationResource(int)
     */
    public float getThumbElevation() {
        return thumbDrawable.getElevation();
    }

    /**
     * Sets the elevation of the thumb.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbElevation
     * @see #getThumbElevation()
     */
    public void setThumbElevation(float elevation) {
        thumbDrawable.setElevation(elevation);
    }

    /**
     * Sets the elevation of the thumb from a dimension resource.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbElevation
     * @see #getThumbElevation()
     */
    public void setThumbElevationResource(@DimenRes int elevation) {
        setThumbElevation(getResources().getDimension(elevation));
    }

    /**
     * Returns the radius of the thumb.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbRadius
     * @see #setThumbRadius(int)
     * @see #setThumbRadiusResource(int)
     */
    @Dimension
    public int getThumbRadius() {
        return thumbRadius;
    }

    /**
     * Sets the radius of the thumb in pixels.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbRadius
     * @see #getThumbRadius()
     */
    public void setThumbRadius(@IntRange(from = 0) @Dimension int radius) {
        if (radius == thumbRadius) {
            return;
        }

        thumbRadius = radius;

        thumbDrawable.setShapeAppearanceModel(
                ShapeAppearanceModel.builder().setAllCorners(CornerFamily.ROUNDED, thumbRadius).build());
        thumbDrawable.setBounds(0, 0, thumbRadius * 2, thumbRadius * 2);

        postInvalidate();
    }

    /**
     * Sets the radius of the thumb from a dimension resource.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbRadius
     * @see #getThumbRadius()
     */
    public void setThumbRadiusResource(@DimenRes int radius) {
        setThumbRadius(getResources().getDimensionPixelSize(radius));
    }

    /**
     * Returns the radius of the halo.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_haloRadius
     * @see #setHaloRadius(int)
     * @see #setHaloRadiusResource(int)
     */
    @Dimension()
    public int getHaloRadius() {
        return haloRadius;
    }

    /**
     * Sets the radius of the halo in pixels.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_haloRadius
     * @see #getHaloRadius()
     */
    public void setHaloRadius(@IntRange(from = 0) @Dimension int radius) {
        if (radius == haloRadius) {
            return;
        }

        haloRadius = radius;
        Drawable background = getBackground();
        if (!shouldDrawCompatHalo() && background instanceof RippleDrawable) {
            DrawableUtils.setRippleDrawableRadius((RippleDrawable) background, haloRadius);
            return;
        }

        postInvalidate();
    }

    /**
     * Sets the radius of the halo from a dimension resource.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_haloRadius
     * @see #getHaloRadius()
     */
    public void setHaloRadiusResource(@DimenRes int radius) {
        setHaloRadius(getResources().getDimensionPixelSize(radius));
    }

    /**
     * Returns the {@link LabelBehavior} used.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_labelBehavior
     * @see #setLabelBehavior(int)
     */
    @LabelBehavior
    public int getLabelBehavior() {
        return labelBehavior;
    }

    /**
     * Determines the {@link LabelBehavior} used.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_labelBehavior
     * @see LabelBehavior
     * @see #getLabelBehavior()
     */
    public void setLabelBehavior(@LabelBehavior int labelBehavior) {
        if (this.labelBehavior != labelBehavior) {
            this.labelBehavior = labelBehavior;
            requestLayout();
        }
    }

    public void setSupportReverse(boolean supportReverse) {
        this.supportReverse = supportReverse;
    }

    /**
     * Returns the side padding of the track.
     */
    @Dimension()
    public int getTrackSidePadding() {
        return trackSidePadding;
    }

    /**
     * Returns the width of the track in pixels.
     */
    @Dimension()
    public int getTrackWidth() {
        return trackWidth;
    }

    /**
     * Returns the height of the track in pixels.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_trackHeight
     * @see #setTrackHeight(int)
     */
    @Dimension()
    public int getTrackHeight() {
        return trackHeight;
    }

    /**
     * Set the height of the track in pixels.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_trackHeight
     * @see #getTrackHeight()
     */
    public void setTrackHeight(@IntRange(from = 0) @Dimension int trackHeight) {
        if (this.trackHeight != trackHeight) {
            this.trackHeight = trackHeight;
            invalidateTrack();
            postInvalidate();
        }
    }

    /**
     * Returns the color of the halo.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_haloColor
     * @see #setHaloTintList(ColorStateList)
     */
    @NonNull
    public ColorStateList getHaloTintList() {
        return haloColor;
    }

    /**
     * Sets the color of the halo.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_haloColor
     * @see #getHaloTintList()
     */
    public void setHaloTintList(@NonNull ColorStateList haloColor) {
        if (haloColor.equals(this.haloColor)) {
            return;
        }

        this.haloColor = haloColor;
        Drawable background = getBackground();
        if (!shouldDrawCompatHalo() && background instanceof RippleDrawable) {
            ((RippleDrawable) background).setColor(haloColor);
            return;
        }

        haloPaint.setColor(getColorForState(haloColor));
        haloPaint.setAlpha(HALO_ALPHA);
        invalidate();
    }

    /**
     * Returns the color of the thumb.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbColor
     * @see #setThumbTintList(ColorStateList)
     */
    @NonNull
    public ColorStateList getThumbTintList() {
        return thumbDrawable.getFillColor();
    }

    /**
     * Sets the color of the thumb.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_thumbColor
     * @see #getThumbTintList()
     */
    public void setThumbTintList(@NonNull ColorStateList thumbColor) {
        thumbDrawable.setFillColor(thumbColor);
    }

    /**
     * Returns the color of the tick if the active and inactive parts aren't different.
     *
     * @throws IllegalStateException If {@code tickColorActive} and {@code tickColorInactive} have
     *                               been set to different values.
     * @attr ref com.google.android.material.R.styleable#Slider_tickColor
     * @see #setTickTintList(ColorStateList)
     * @see #setTickInactiveTintList(ColorStateList)
     * @see #setTickActiveTintList(ColorStateList)
     * @see #getTickInactiveTintList()
     * @see #getTickActiveTintList()
     */
    @NonNull
    public ColorStateList getTickTintList() {
        if (!tickColorInactive.equals(tickColorActive)) {
            throw new IllegalStateException(
                    "The inactive and active ticks are different colors. Use the getTickColorInactive() and"
                            + " getTickColorActive() methods instead.");
        }
        return tickColorActive;
    }

    /**
     * Sets the color of the tick marks.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_tickColor
     * @see #setTickInactiveTintList(ColorStateList)
     * @see #setTickActiveTintList(ColorStateList)
     * @see #getTickTintList()
     */
    public void setTickTintList(@NonNull ColorStateList tickColor) {
        setTickInactiveTintList(tickColor);
        setTickActiveTintList(tickColor);
    }

    /**
     * Returns the color of the ticks on the active portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_tickColorActive
     * @see #setTickActiveTintList(ColorStateList)
     * @see #setTickTintList(ColorStateList)
     * @see #getTickTintList()
     */
    @NonNull
    public ColorStateList getTickActiveTintList() {
        return tickColorActive;
    }

    /**
     * Sets the color of the ticks on the active portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_tickColorActive
     * @see #getTickActiveTintList()
     * @see #setTickTintList(ColorStateList)
     */
    public void setTickActiveTintList(@NonNull ColorStateList tickColor) {
        if (tickColor.equals(tickColorActive)) {
            return;
        }
        tickColorActive = tickColor;
        activeTicksPaint.setColor(getColorForState(tickColorActive));
        invalidate();
    }

    /**
     * Returns the color of the ticks on the inactive portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_tickColorInactive
     * @see #setTickInactiveTintList(ColorStateList)
     * @see #setTickTintList(ColorStateList)
     * @see #getTickTintList()
     */
    @NonNull
    public ColorStateList getTickInactiveTintList() {
        return tickColorInactive;
    }

    /**
     * Sets the color of the ticks on the inactive portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_tickColorInactive
     * @see #getTickInactiveTintList()
     * @see #setTickTintList(ColorStateList)
     */
    public void setTickInactiveTintList(@NonNull ColorStateList tickColor) {
        if (tickColor.equals(tickColorInactive)) {
            return;
        }
        tickColorInactive = tickColor;
        inactiveTicksPaint.setColor(getColorForState(tickColorInactive));
        invalidate();
    }

    /**
     * Returns the color of the track if the active and inactive parts aren't different.
     *
     * @throws IllegalStateException If {@code trackColorActive} and {@code trackColorInactive} have
     *                               been set to different values.
     * @attr ref com.google.android.material.R.styleable#Slider_trackColor
     * @see #setTrackTintList(ColorStateList)
     * @see #setTrackInactiveTintList(ColorStateList)
     * @see #setTrackActiveTintList(ColorStateList)
     * @see #getTrackInactiveTintList()
     * @see #getTrackActiveTintList()
     */
    @NonNull
    public ColorStateList getTrackTintList() {
        if (!trackColorInactive.equals(trackColorActive)) {
            throw new IllegalStateException(
                    "The inactive and active parts of the track are different colors. Use the"
                            + " getInactiveTrackColor() and getActiveTrackColor() methods instead.");
        }
        return trackColorActive;
    }

    /**
     * Sets the color of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_trackColor
     * @see #setTrackInactiveTintList(ColorStateList)
     * @see #setTrackActiveTintList(ColorStateList)
     * @see #getTrackTintList()
     */
    public void setTrackTintList(@NonNull ColorStateList trackColor) {
        setTrackInactiveTintList(trackColor);
        setTrackActiveTintList(trackColor);
    }

    /**
     * Returns the color of the active portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_trackColorActive
     * @see #setTrackActiveTintList(ColorStateList)
     * @see #setTrackTintList(ColorStateList)
     * @see #getTrackTintList()
     */
    @NonNull
    public ColorStateList getTrackActiveTintList() {
        return trackColorActive;
    }

    /**
     * Sets the color of the active portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_trackColorActive
     * @see #getTrackActiveTintList()
     * @see #setTrackTintList(ColorStateList)
     */
    public void setTrackActiveTintList(@NonNull ColorStateList trackColor) {
        if (trackColor.equals(trackColorActive)) {
            return;
        }
        trackColorActive = trackColor;
        activeTrackPaint.setColor(getColorForState(trackColorActive));
        invalidate();
    }

    /**
     * Returns the color of the inactive portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_trackColorInactive
     * @see #setTrackInactiveTintList(ColorStateList)
     * @see #setTrackTintList(ColorStateList)
     * @see #getTrackTintList()
     */
    @NonNull
    public ColorStateList getTrackInactiveTintList() {
        return trackColorInactive;
    }

    /**
     * Sets the color of the inactive portion of the track.
     *
     * @attr ref com.google.android.material.R.styleable#Slider_trackColorInactive
     * @see #getTrackInactiveTintList()
     * @see #setTrackTintList(ColorStateList)
     */
    public void setTrackInactiveTintList(@NonNull ColorStateList trackColor) {
        if (trackColor.equals(trackColorInactive)) {
            return;
        }
        trackColorInactive = trackColor;
        inactiveTrackPaint.setColor(getColorForState(trackColorInactive));
        invalidate();
    }

    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        // When we're disabled, set the layer type to hardware so we can clear the track out from behind
        // the thumb.
        setLayerType(enabled ? LAYER_TYPE_NONE : LAYER_TYPE_HARDWARE, null);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        // The label is attached on the Overlay relative to the content.
        for (TooltipDrawable label : labels) {
            attachLabelToContentView(label);
        }
    }

    private void attachLabelToContentView(TooltipDrawable label) {
        label.setRelativeToView(ViewUtils.getContentView(this));
    }

    @Override
    protected void onDetachedFromWindow() {
        if (accessibilityEventSender != null) {
            removeCallbacks(accessibilityEventSender);
        }

        for (TooltipDrawable label : labels) {
            detachLabelFromContentView(label);
        }

        super.onDetachedFromWindow();
    }

    private void detachLabelFromContentView(TooltipDrawable label) {
        ViewOverlayImpl contentViewOverlay = ViewUtils.getContentViewOverlay(this);
        if (contentViewOverlay != null) {
            contentViewOverlay.remove(label);
            label.detachView(ViewUtils.getContentView(this));
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(
                widthMeasureSpec,
                MeasureSpec.makeMeasureSpec(
                        widgetHeight
                                + (labelBehavior == LABEL_WITHIN_BOUNDS ? labels.get(0).getIntrinsicHeight() : 0),
                        MeasureSpec.EXACTLY));
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // Update the visible track width.
        trackWidth = Math.max(w - trackSidePadding * 2, 0);

        // Update the visible tick coordinates.
        if (stepSize > 0.0f) {
            calculateTicksCoordinates();
        }

        updateHaloHotspot();
    }

    private void calculateTicksCoordinates() {
        validateConfigurationIfDirty();

        int tickCount = (int) ((valueTo - valueFrom) / stepSize + 1);
        // Limit the tickCount if they will be too dense.
        tickCount = Math.min(tickCount, trackWidth / (trackHeight * 2) + 1);
        if (ticksCoordinates == null || ticksCoordinates.length != tickCount * 2) {
            ticksCoordinates = new float[tickCount * 2];
        }

        float interval = trackWidth / (float) (tickCount - 1);
        for (int i = 0; i < tickCount * 2; i += 2) {
            ticksCoordinates[i] = trackSidePadding + i / 2 * interval;
            ticksCoordinates[i + 1] = calculateTop();
        }
    }

    private void updateHaloHotspot() {
        // Set the hotspot as the halo if RippleDrawable is being used.
        if (!shouldDrawCompatHalo() && getMeasuredWidth() > 0) {
            final Drawable background = getBackground();
            if (background instanceof RippleDrawable) {
                int x = (int) (normalizeValue(values.get(focusedThumbIdx)) * trackWidth + trackSidePadding);
                int y = calculateTop();
                DrawableCompat.setHotspotBounds(
                        background, x - haloRadius, y - haloRadius, x + haloRadius, y + haloRadius);
            }
        }
    }

    private int calculateTop() {
        return trackTop
                + (labelBehavior == LABEL_WITHIN_BOUNDS ? labels.get(0).getIntrinsicHeight() : 0);
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        if (dirtyConfig) {
            validateConfigurationIfDirty();

            // Update the visible tick coordinates.
            if (stepSize > 0.0f) {
                calculateTicksCoordinates();
            }
        }

        super.onDraw(canvas);

        int top = calculateTop();

        drawInactiveTrack(canvas, trackWidth, top);
        if (Collections.max(getValues()) > valueFrom) {
            drawActiveTrack(canvas, trackWidth, top);
        }

        if (stepSize > 0.0f) {
            drawTicks(canvas);
        }

        if ((thumbIsPressed || isFocused()) && isEnabled()) {
            maybeDrawHalo(canvas, trackWidth, top);

            // Draw labels if there is an active thumb.
            if (activeThumbIdx != -1) {
                ensureLabels();
            }
        }

        drawThumbs(canvas, trackWidth, top);
    }

    /**
     * Returns a float array where {@code float[0]} is the normalized left position and {@code
     * float[1]} is the normalized right position of the range.
     */
    private float[] getActiveRange() {
        float max = Collections.max(getValues());
        float min = Collections.min(getValues());
        float left = normalizeValue(values.size() == 1 ? valueFrom : min);
        float right = normalizeValue(max);

        // In RTL we draw things in reverse, so swap the left and right range values
        return isRtl() ? new float[]{right, left} : new float[]{left, right};
    }

    private void drawInactiveTrack(@NonNull Canvas canvas, int width, int top) {
        float[] activeRange = getActiveRange();
        if (supportReverse && values.get(1) < values.get(0)) {
            float right = trackSidePadding + activeRange[1] * width;
            float left = trackSidePadding + activeRange[0] * width;
            canvas.drawLine(left, top, right, top, inactiveTrackPaint);
        } else {
            float right = trackSidePadding + activeRange[1] * width;
            if (right < trackSidePadding + width) {
                canvas.drawLine(right, top, trackSidePadding + width, top, inactiveTrackPaint);
            }

            // Also draw inactive track to the left if there is any
            float left = trackSidePadding + activeRange[0] * width;
            if (left > trackSidePadding) {
                canvas.drawLine(trackSidePadding, top, left, top, inactiveTrackPaint);
            }
        }
    }

    /**
     * Returns a number between 0 and 1 indicating where on the track this value should sit with 0
     * being on the far left, and 1 on the far right.
     */
    private float normalizeValue(float value) {
        float normalized = (value - valueFrom) / (valueTo - valueFrom);
        if (isRtl()) {
            return 1 - normalized;
        }
        return normalized;
    }

    private void drawActiveTrack(@NonNull Canvas canvas, int width, int top) {
        float[] activeRange = getActiveRange();
        if (supportReverse && values.get(1) < values.get(0)) {
            float right = trackSidePadding + activeRange[1] * width;
            if (right < trackSidePadding + width) {
                canvas.drawLine(right, top, trackSidePadding + width, top, activeTrackPaint);
            }
            // Also draw inactive track to the left if there is any
            float left = trackSidePadding + activeRange[0] * width;
            if (left > trackSidePadding) {
                canvas.drawLine(trackSidePadding, top, left, top, activeTrackPaint);
            }
        } else {
            float right = trackSidePadding + activeRange[1] * width;
            float left = trackSidePadding + activeRange[0] * width;
            canvas.drawLine(left, top, right, top, activeTrackPaint);
        }
    }

    private void drawTicks(@NonNull Canvas canvas) {
        float[] activeRange = getActiveRange();
        int leftPivotIndex = pivotIndex(ticksCoordinates, activeRange[0]);
        int rightPivotIndex = pivotIndex(ticksCoordinates, activeRange[1]);

        if (supportReverse && values.get(1) < values.get(0)) {

            // Draw inactive ticks to the left of the smallest thumb.
            canvas.drawPoints(ticksCoordinates, 0, leftPivotIndex * 2, activeTicksPaint);

            // Draw active ticks between the thumbs.
            canvas.drawPoints(
                    ticksCoordinates,
                    leftPivotIndex * 2,
                    rightPivotIndex * 2 - leftPivotIndex * 2,
                    inactiveTicksPaint);

            // Draw inactive ticks to the right of the largest thumb.
            canvas.drawPoints(
                    ticksCoordinates,
                    rightPivotIndex * 2,
                    ticksCoordinates.length - rightPivotIndex * 2,
                    activeTicksPaint);
        } else {
            // Draw inactive ticks to the left of the smallest thumb.
            canvas.drawPoints(ticksCoordinates, 0, leftPivotIndex * 2, inactiveTicksPaint);

            // Draw active ticks between the thumbs.
            canvas.drawPoints(
                    ticksCoordinates,
                    leftPivotIndex * 2,
                    rightPivotIndex * 2 - leftPivotIndex * 2,
                    activeTicksPaint);

            // Draw inactive ticks to the right of the largest thumb.
            canvas.drawPoints(
                    ticksCoordinates,
                    rightPivotIndex * 2,
                    ticksCoordinates.length - rightPivotIndex * 2,
                    inactiveTicksPaint);
        }
    }

    private void drawThumbs(@NonNull Canvas canvas, int width, int top) {
        // Clear out the track behind the thumb if we're in a disable state since the thumb is
        // transparent.
        if (!isEnabled()) {
            for (Float value : values) {
                canvas.drawCircle(
                        trackSidePadding + normalizeValue(value) * width, top, thumbRadius, thumbPaint);
            }
        }

        for (Float value : values) {
            canvas.save();
            canvas.translate(
                    trackSidePadding + (int) (normalizeValue(value) * width) - thumbRadius,
                    top - thumbRadius);
            thumbDrawable.draw(canvas);
            canvas.restore();
        }
    }

    private void maybeDrawHalo(@NonNull Canvas canvas, int width, int top) {
        // Only draw the halo for devices that aren't using the ripple.
        if (shouldDrawCompatHalo()) {
            int centerX = (int) (trackSidePadding + normalizeValue(values.get(focusedThumbIdx)) * width);
            if (VERSION.SDK_INT < VERSION_CODES.P) {
                // In this case we can clip the rect to allow drawing outside the bounds.
                canvas.clipRect(
                        centerX - haloRadius,
                        top - haloRadius,
                        centerX + haloRadius,
                        top + haloRadius,
                        Op.UNION);
            }
            canvas.drawCircle(centerX, top, haloRadius, haloPaint);
        }
    }

    private boolean shouldDrawCompatHalo() {
        return forceDrawCompatHalo
                || VERSION.SDK_INT < VERSION_CODES.LOLLIPOP
                || !(getBackground() instanceof RippleDrawable);
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (!isEnabled()) {
            return false;
        }
        float x = event.getX();
        touchPosition = (x - trackSidePadding) / trackWidth;
        touchPosition = Math.max(0, touchPosition);
        touchPosition = Math.min(1, touchPosition);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                touchDownX = x;

                // If we're inside a scrolling container,
                // we should start dragging in ACTION_MOVE
                if (isInScrollingContainer()) {
                    break;
                }

                getParent().requestDisallowInterceptTouchEvent(true);

                if (!pickActiveThumb()) {
                    // Couldn't determine the active thumb yet.
                    break;
                }

                requestFocus();
                thumbIsPressed = true;
                snapTouchPosition();
                updateHaloHotspot();
                invalidate();
                onStartTrackingTouch();
                break;
            case MotionEvent.ACTION_MOVE:
                if (!thumbIsPressed) {
                    // Check if we're trying to scroll instead of dragging this Slider
                    if (abs(x - touchDownX) < scaledTouchSlop) {
                        return false;
                    }
                    getParent().requestDisallowInterceptTouchEvent(true);
                    onStartTrackingTouch();
                }

                if (!pickActiveThumb()) {
                    // Couldn't determine the active thumb yet.
                    break;
                }

                thumbIsPressed = true;
                snapTouchPosition();
                updateHaloHotspot();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                thumbIsPressed = false;
                // We need to handle a tap if the last event was down at the same point.
                if (lastEvent != null
                        && lastEvent.getActionMasked() == MotionEvent.ACTION_DOWN
                        && abs(lastEvent.getX() - event.getX()) <= scaledTouchSlop
                        && abs(lastEvent.getY() - event.getY()) <= scaledTouchSlop) {
                    pickActiveThumb();
                }

                if (activeThumbIdx != -1) {
                    snapTouchPosition();
                    activeThumbIdx = -1;
                }
                for (TooltipDrawable label : labels) {
                    ViewUtils.getContentViewOverlay(this).remove(label);
                }
                onStopTrackingTouch();
                invalidate();
                break;
            default:
                // Nothing to do in this case.
        }

        // Set if the thumb is pressed. This will cause the ripple to be drawn.
        setPressed(thumbIsPressed);

        lastEvent = MotionEvent.obtain(event);
        return true;
    }

    /**
     * Calculates the index the closest tick coordinates that the thumb should snap to.
     *
     * @param coordinates Tick coordinates defined in {@code #setTicksCoordinates()}.
     * @param position    Actual thumb position.
     * @return Index of the closest tick coordinate.
     */
    private static int pivotIndex(float[] coordinates, float position) {
        return Math.round(position * (coordinates.length / 2 - 1));
    }

    private double snapPosition(float position) {
        if (stepSize > 0.0f) {
            int stepCount = (int) ((valueTo - valueFrom) / stepSize);
            return Math.round(position * stepCount) / (double) stepCount;
        }

        return position;
    }

    /**
     * Tries to pick the active thumb if one hasn't already been set. This will pick the closest thumb
     * if there is only one thumb under the touch position. If there is more than one thumb under the
     * touch position, it will wait for enough drag left or right to determine which thumb to pick.
     */
    protected boolean pickActiveThumb() {
        if (activeThumbIdx != -1) {
            return true;
        }

        float touchValue = getValueOfTouchPositionAbsolute();
        float touchX = valueToX(touchValue);
        activeThumbIdx = 0;
        float activeThumbDiff = abs(values.get(activeThumbIdx) - touchValue);
        for (int i = 1; i < values.size(); i++) {
            float valueDiff = abs(values.get(i) - touchValue);
            float valueX = valueToX(values.get(i));
            if (compare(valueDiff, activeThumbDiff) > 1) {
                break;
            }

            boolean movingForward = isRtl() ? (valueX - touchX) > 0 : (valueX - touchX) < 0;
            // Keep replacing the activeThumbIdx, while the diff decreases.
            // If the diffs are equal we'll pick the thumb based on which direction we are dragging.
            if (compare(valueDiff, activeThumbDiff) < 0) {
                activeThumbDiff = valueDiff;
                activeThumbIdx = i;
                continue;
            }

            if (compare(valueDiff, activeThumbDiff) == 0) {
                // Two thumbs on the same value and we don't have enough movement to use direction yet.
                if (abs(valueX - touchX) < scaledTouchSlop) {
                    activeThumbIdx = -1;
                    return false;
                }

                if (movingForward) {
                    activeThumbDiff = valueDiff;
                    activeThumbIdx = i;
                }
            }
        }

        return activeThumbIdx != -1;
    }

    private float getValueOfTouchPositionAbsolute() {
        float position = touchPosition;
        if (isRtl()) {
            position = 1 - position;
        }
        return (position * (valueTo - valueFrom) + valueFrom);
    }

    /**
     * Snaps the thumb position to the closest tick coordinates in discrete mode, and the input
     * position in continus
     *
     * @return true, if {@code #thumbPosition is updated}; false, otherwise.
     */
    private boolean snapTouchPosition() {
        return snapActiveThumbToValue(getValueOfTouchPosition());
    }

    private boolean snapActiveThumbToValue(float value) {
        return snapThumbToValue(activeThumbIdx, value);
    }

    private boolean snapThumbToValue(int idx, float value) {
        // Check if the new value equals a value that was already set.
        if (abs(value - values.get(idx)) < THRESHOLD) {
            return false;
        }

        float newValue = supportReverse ? value : getClampedValue(idx, value);
        // Replace the old value with the new value of the touch position.
        values.set(idx, newValue);
        focusedThumbIdx = idx;

        dispatchOnChangedFromUser(idx);
        return true;
    }

    /**
     * Thumbs cannot cross each other, clamp the value to a bound or the value next to it.
     */
    private float getClampedValue(int idx, float value) {
        float upperBound = idx + 1 >= values.size() ? valueTo : values.get(idx + 1);
        float lowerBound = idx - 1 < 0 ? valueFrom : values.get(idx - 1);
        return clamp(value, lowerBound, upperBound);
    }

    private float getValueOfTouchPosition() {
        double position = snapPosition(touchPosition);

        // We might need to invert the touch position to get the correct value.
        if (isRtl()) {
            position = 1 - position;
        }
        return (float) (position * (valueTo - valueFrom) + valueFrom);
    }

    private float valueToX(float value) {
        return normalizeValue(value) * trackWidth + trackSidePadding;
    }

    private void ensureLabels() {
        if (labelBehavior == LABEL_GONE) {
            // If the label shouldn't be drawn we can skip this.
            return;
        }

        Iterator<TooltipDrawable> labelItr = labels.iterator();

        for (int i = 0; i < values.size() && labelItr.hasNext(); i++) {
            if (i == focusedThumbIdx) {
                // We position the focused thumb last so it's displayed on top, so skip it for now.
                continue;
            }

            setValueForLabel(labelItr.next(), values.get(i));
        }

        if (!labelItr.hasNext()) {
            throw new IllegalStateException(
                    String.format(
                            "Not enough labels(%d) to display all the values(%d)", labels.size(), values.size()));
        }

        // Now set the label for the focused thumb so it's on top.
        setValueForLabel(labelItr.next(), values.get(focusedThumbIdx));
    }

    private String formatValue(float value) {
        if (hasLabelFormatter()) {
            return formatter.getFormattedValue(value);
        }

        return String.format((int) value == value ? "%.0f" : "%.2f", value);
    }

    private void setValueForLabel(TooltipDrawable label, float value) {
        label.setText(formatValue(value));

        int left =
                trackSidePadding
                        + (int) (normalizeValue(value) * trackWidth)
                        - label.getIntrinsicWidth() / 2;
        int top = calculateTop() - (labelPadding + thumbRadius);
        label.setBounds(left, top - label.getIntrinsicHeight(), left + label.getIntrinsicWidth(), top);

        // Calculate the difference between the bounds of this view and the bounds of the root view to
        // correctly position this view in the overlay layer.
        Rect rect = new Rect(label.getBounds());
        DescendantOffsetUtils.offsetDescendantRect(ViewUtils.getContentView(this), this, rect);
        label.setBounds(rect);

        ViewUtils.getContentViewOverlay(this).add(label);
    }

    private void invalidateTrack() {
        inactiveTrackPaint.setStrokeWidth(trackHeight);
        activeTrackPaint.setStrokeWidth(trackHeight);
        inactiveTicksPaint.setStrokeWidth(trackHeight / 2.0f);
        activeTicksPaint.setStrokeWidth(trackHeight / 2.0f);
    }

    /**
     * If this returns true, we can't start dragging the Slider immediately when we receive a {@link
     * MotionEvent#ACTION_DOWN}. Instead, we must wait for a {@link MotionEvent#ACTION_MOVE}. Copied
     * from hidden method of {@link View} isInScrollingContainer.
     *
     * @return true if any of this View's parents is a scrolling View.
     */
    private boolean isInScrollingContainer() {
        ViewParent p = getParent();
        while (p instanceof ViewGroup) {
            if (((ViewGroup) p).shouldDelayChildPressedState()) {
                return true;
            }
            p = p.getParent();
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    private void dispatchOnChangedProgramatically() {
        for (L listener : changeListeners) {
            for (Float value : values) {
                listener.onValueChange((S) this, value, false);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void dispatchOnChangedFromUser(int idx) {
        for (L listener : changeListeners) {
            listener.onValueChange((S) this, values.get(idx), true);
        }
        if (accessibilityManager != null && accessibilityManager.isEnabled()) {
            scheduleAccessibilityEventSender(idx);
        }
    }

    @SuppressWarnings("unchecked")
    private void onStartTrackingTouch() {
        for (T listener : touchListeners) {
            listener.onStartTrackingTouch((S) this);
        }
    }

    @SuppressWarnings("unchecked")
    private void onStopTrackingTouch() {
        for (T listener : touchListeners) {
            listener.onStopTrackingTouch((S) this);
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();

        inactiveTrackPaint.setColor(getColorForState(trackColorInactive));
        activeTrackPaint.setColor(getColorForState(trackColorActive));
        inactiveTicksPaint.setColor(getColorForState(tickColorInactive));
        activeTicksPaint.setColor(getColorForState(tickColorActive));
        for (TooltipDrawable label : labels) {
            if (label.isStateful()) {
                label.setState(getDrawableState());
            }
        }
        if (thumbDrawable.isStateful()) {
            thumbDrawable.setState(getDrawableState());
        }
        haloPaint.setColor(getColorForState(haloColor));
        haloPaint.setAlpha(HALO_ALPHA);
    }

    @ColorInt
    private int getColorForState(@NonNull ColorStateList colorStateList) {
        return colorStateList.getColorForState(getDrawableState(), colorStateList.getDefaultColor());
    }

    @VisibleForTesting
    void forceDrawCompatHalo(boolean force) {
        forceDrawCompatHalo = force;
    }

    @Override
    public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
        if (!isEnabled()) {
            return super.onKeyDown(keyCode, event);
        }

        // If there's only one thumb, we can select it right away.
        if (values.size() == 1) {
            activeThumbIdx = 0;
        }

        // If there is no active thumb, key events will be used to pick the thumb to change.
        if (activeThumbIdx == -1) {
            Boolean handled = onKeyDownNoActiveThumb(keyCode, event);
            return handled != null ? handled : super.onKeyDown(keyCode, event);
        }

        isLongPress |= event.isLongPress();
        Float increment = calculateIncrementForKey(keyCode);
        if (increment != null) {
            if (snapActiveThumbToValue(values.get(activeThumbIdx) + increment)) {
                updateHaloHotspot();
                postInvalidate();
            }
            return true;
        }
        switch (keyCode) {
            case KeyEvent.KEYCODE_TAB:
                if (event.hasNoModifiers()) {
                    return moveFocus(1);
                }

                if (event.isShiftPressed()) {
                    return moveFocus(-1);
                }
                return false;
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                activeThumbIdx = -1;
                for (TooltipDrawable label : labels) {
                    ViewUtils.getContentViewOverlay(this).remove(label);
                }
                postInvalidate();
                return true;
            default:
                // Nothing to do in this case.
        }

        return super.onKeyDown(keyCode, event);
    }

    private Boolean onKeyDownNoActiveThumb(int keyCode, @NonNull KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_TAB:
                if (event.hasNoModifiers()) {
                    return moveFocus(1);
                }

                if (event.isShiftPressed()) {
                    return moveFocus(-1);
                }
                return false;
            case KeyEvent.KEYCODE_DPAD_LEFT:
                moveFocusInAbsoluteDirection(-1);
                return true;
            case KeyEvent.KEYCODE_MINUS:
                moveFocus(-1);
                return true;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                moveFocusInAbsoluteDirection(1);
                return true;
            case KeyEvent.KEYCODE_EQUALS:
                // Numpad Plus == Shift + Equals, at least in AVD, so fall through.
            case KeyEvent.KEYCODE_PLUS:
                moveFocus(1);
                return true;
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                activeThumbIdx = focusedThumbIdx;
                postInvalidate();
                return true;
            default:
                // Nothing to do in this case.
        }

        return null;
    }

    @Override
    public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
        isLongPress = false;
        return super.onKeyUp(keyCode, event);
    }

    final boolean isRtl() {
        return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
    }

    /**
     * Attempts to move focus to next or previous thumb <i>independent of layout direction</i>
     * and returns whether the focused thumb changed.
     * If focused thumb didn't change, we're at the view boundary for specified {@code direction}
     * and focus may be moved to next or previous view instead.
     *
     * @see #moveFocusInAbsoluteDirection(int)
     */
    private boolean moveFocus(int direction) {
        int oldFocusedThumbIdx = focusedThumbIdx;
        // Prevent integer overflow.
        final long newFocusedThumbIdx = (long) oldFocusedThumbIdx + direction;
        focusedThumbIdx = (int) clamp(newFocusedThumbIdx, 0, values.size() - 1);
        if (focusedThumbIdx == oldFocusedThumbIdx) {
            // Move focus to next or previous view.
            return false;
        }
        if (activeThumbIdx != -1) {
            activeThumbIdx = focusedThumbIdx;
        }
        updateHaloHotspot();
        postInvalidate();
        return true;
    }

    /**
     * Attempts to move focus to the <i>left or right</i> of currently focused thumb
     * and returns whether the focused thumb changed.
     * If focused thumb didn't change, we're at the view boundary for specified {@code direction}
     * and focus may be moved to next or previous view instead.
     *
     * @see #moveFocus(int)
     */
    private boolean moveFocusInAbsoluteDirection(int direction) {
        if (isRtl()) {
            // Prevent integer overflow.
            direction = direction == Integer.MIN_VALUE ? Integer.MAX_VALUE : -direction;
        }
        return moveFocus(direction);
    }

    private Float calculateIncrementForKey(int keyCode) {
        // If this is a long press, increase the increment so it will only take around 20 steps.
        // Otherwise choose the smallest valid increment.
        float increment = isLongPress ? calculateStepIncrement(20) : calculateStepIncrement();
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_LEFT:
                return isRtl() ? increment : -increment;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                return isRtl() ? -increment : increment;
            case KeyEvent.KEYCODE_MINUS:
                return -increment;
            case KeyEvent.KEYCODE_EQUALS:
                // Numpad Plus == Shift + Equals, at least in AVD, so fall through.
            case KeyEvent.KEYCODE_PLUS:
                return increment;
            default:
                return null;
        }
    }

    /**
     * Returns a small valid step increment to use when adding an offset to an existing value
     */
    private float calculateStepIncrement() {
        return stepSize == 0 ? 1 : stepSize;
    }

    /**
     * Returns a valid increment based on the {@code stepSize} (if it's set) that will allow
     * approximately {@code stepFactor} steps to cover the whole range.
     */
    private float calculateStepIncrement(int stepFactor) {
        float increment = calculateStepIncrement();
        float numSteps = (valueTo - valueFrom) / increment;
        if (numSteps <= stepFactor) {
            return increment;
        }

        return Math.round((numSteps / stepFactor)) * increment;
    }

    @Override
    protected void onFocusChanged(
            boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
        if (!gainFocus) {
            activeThumbIdx = -1;
            for (TooltipDrawable label : labels) {
                ViewUtils.getContentViewOverlay(this).remove(label);
            }
            accessibilityHelper.clearKeyboardFocusForVirtualView(focusedThumbIdx);
        } else {
            focusThumbOnFocusGained(direction);
            accessibilityHelper.requestKeyboardFocusForVirtualView(focusedThumbIdx);
        }
    }

    private void focusThumbOnFocusGained(int direction) {
        switch (direction) {
            case FOCUS_BACKWARD:
                moveFocus(Integer.MAX_VALUE);
                break;
            case FOCUS_LEFT:
                moveFocusInAbsoluteDirection(Integer.MAX_VALUE);
                break;
            case FOCUS_FORWARD:
                moveFocus(Integer.MIN_VALUE);
                break;
            case FOCUS_RIGHT:
                moveFocusInAbsoluteDirection(Integer.MIN_VALUE);
                break;
            case FOCUS_UP:
            case FOCUS_DOWN:
            default:
                // Don't make assumptions about where exactly focus came from. Use previously focused thumb.
        }
    }

    @VisibleForTesting
    final int getAccessibilityFocusedVirtualViewId() {
        return accessibilityHelper.getAccessibilityFocusedVirtualViewId();
    }

    @NonNull
    @Override
    public CharSequence getAccessibilityClassName() {
        return SeekBar.class.getName();
    }

    @Override
    public boolean dispatchHoverEvent(@NonNull MotionEvent event) {
        return accessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
    }

    @Override
    public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
        // We explicitly don't pass the key event to the accessibilityHelper because it doesn't handle
        // focus correctly in some cases (Such as moving left after moving right a few times).
        return super.dispatchKeyEvent(event);
    }

    /**
     * Schedule a command for sending an accessibility event. </br> Note: A command is used to ensure
     * that accessibility events are sent at most one in a given time frame to save system resources
     * while the value changes quickly.
     */
    private void scheduleAccessibilityEventSender(int idx) {
        if (accessibilityEventSender == null) {
            accessibilityEventSender = new AccessibilityEventSender();
        } else {
            removeCallbacks(accessibilityEventSender);
        }
        accessibilityEventSender.setVirtualViewId(idx);
        postDelayed(accessibilityEventSender, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
    }

    /**
     * Command for sending an accessibility event.
     */
    private class AccessibilityEventSender implements Runnable {
        int virtualViewId = -1;

        void setVirtualViewId(int virtualViewId) {
            this.virtualViewId = virtualViewId;
        }

        @Override
        public void run() {
            accessibilityHelper.sendEventForVirtualView(
                    virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SliderState sliderState = new SliderState(superState);
        sliderState.valueFrom = valueFrom;
        sliderState.valueTo = valueTo;
        sliderState.values = new ArrayList<>(values);
        sliderState.stepSize = stepSize;
        sliderState.hasFocus = hasFocus();
        return sliderState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SliderState sliderState = (SliderState) state;
        super.onRestoreInstanceState(sliderState.getSuperState());

        valueFrom = sliderState.valueFrom;
        valueTo = sliderState.valueTo;
        setValuesInternal(sliderState.values);
        stepSize = sliderState.stepSize;
        if (sliderState.hasFocus) {
            requestFocus();
        }
        dispatchOnChangedProgramatically();
    }

    static class SliderState extends BaseSavedState {

        float valueFrom;
        float valueTo;
        ArrayList<Float> values;
        float stepSize;
        boolean hasFocus;

        public static final Creator<SliderState> CREATOR =
                new Creator<SliderState>() {

                    @NonNull
                    @Override
                    public SliderState createFromParcel(@NonNull Parcel source) {
                        return new SliderState(source);
                    }

                    @NonNull
                    @Override
                    public SliderState[] newArray(int size) {
                        return new SliderState[size];
                    }
                };

        SliderState(Parcelable superState) {
            super(superState);
        }

        private SliderState(@NonNull Parcel source) {
            super(source);
            valueFrom = source.readFloat();
            valueTo = source.readFloat();
            values = new ArrayList<>();
            source.readList(values, Float.class.getClassLoader());
            stepSize = source.readFloat();
            hasFocus = source.createBooleanArray()[0];
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeFloat(valueFrom);
            dest.writeFloat(valueTo);
            dest.writeList(values);
            dest.writeFloat(stepSize);
            boolean[] booleans = new boolean[1];
            booleans[0] = hasFocus;
            dest.writeBooleanArray(booleans);
        }
    }

    void updateBoundsForVirturalViewId(int virtualViewId, Rect virtualViewBounds) {
        int x = trackSidePadding + (int) (normalizeValue(getValues().get(virtualViewId)) * trackWidth);
        int y = calculateTop();

        virtualViewBounds.set(x - thumbRadius, y - thumbRadius, x + thumbRadius, y + thumbRadius);
    }

    private static class AccessibilityHelper extends ExploreByTouchHelper {

        private final BaseSlider<?, ?, ?> slider;
        Rect virtualViewBounds = new Rect();

        AccessibilityHelper(BaseSlider<?, ?, ?> slider) {
            super(slider);
            this.slider = slider;
        }

        @Override
        protected int getVirtualViewAt(float x, float y) {
            for (int i = 0; i < slider.getValues().size(); i++) {
                slider.updateBoundsForVirturalViewId(i, virtualViewBounds);
                if (virtualViewBounds.contains((int) x, (int) y)) {
                    return i;
                }
            }

            return ExploreByTouchHelper.HOST_ID;
        }

        @Override
        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
            for (int i = 0; i < slider.getValues().size(); i++) {
                virtualViewIds.add(i);
            }
        }

        @Override
        protected void onPopulateNodeForVirtualView(
                int virtualViewId, AccessibilityNodeInfoCompat info) {

            info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS);

            List<Float> values = slider.getValues();
            final float value = values.get(virtualViewId);
            float valueFrom = slider.getValueFrom();
            float valueTo = slider.getValueTo();

            if (slider.isEnabled()) {
                if (value > valueFrom) {
                    info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
                }
                if (value < valueTo) {
                    info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
                }
            }

            info.setRangeInfo(RangeInfoCompat.obtain(RANGE_TYPE_FLOAT, valueFrom, valueTo, value));
            info.setClassName(SeekBar.class.getName());
            StringBuilder contentDescription = new StringBuilder();
            // Add the content description of the slider.
            if (slider.getContentDescription() != null) {
                contentDescription.append(slider.getContentDescription()).append(",");
            }
            // Add the range to the content description.
            if (values.size() > 1) {
                contentDescription.append(startOrEndDescription(virtualViewId));
                contentDescription.append(slider.formatValue(value));
            }
            info.setContentDescription(contentDescription.toString());

            slider.updateBoundsForVirturalViewId(virtualViewId, virtualViewBounds);
            info.setBoundsInParent(virtualViewBounds);
        }

        @NonNull
        private String startOrEndDescription(int virtualViewId) {
            List<Float> values = slider.getValues();
            if (virtualViewId == values.size() - 1) {
                return slider.getContext().getString(R.string.material_slider_range_end);
            }

            if (virtualViewId == 0) {
                return slider.getContext().getString(R.string.material_slider_range_start);
            }

            return "";
        }

        @Override
        protected boolean onPerformActionForVirtualView(
                int virtualViewId, int action, Bundle arguments) {
            if (!slider.isEnabled()) {
                return false;
            }

            switch (action) {
                case android.R.id.accessibilityActionSetProgress: {
                    if (arguments == null
                            || !arguments.containsKey(
                            AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE)) {
                        return false;
                    }
                    float value =
                            arguments.getFloat(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE);
                    if (slider.snapThumbToValue(virtualViewId, value)) {
                        slider.updateHaloHotspot();
                        slider.postInvalidate();
                        invalidateVirtualView(virtualViewId);
                        return true;
                    }
                    return false;
                }
                case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
                case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
                    float increment = slider.calculateStepIncrement(20);
                    if (action == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) {
                        increment = -increment;
                    }

                    // Swap the increment if we're in RTL.
                    if (slider.isRtl()) {
                        increment = -increment;
                    }

                    List<Float> values = slider.getValues();
                    float clamped =
                            clamp(
                                    values.get(virtualViewId) + increment,
                                    slider.getValueFrom(),
                                    slider.getValueTo());
                    if (slider.snapThumbToValue(virtualViewId, clamped)) {
                        slider.updateHaloHotspot();
                        slider.postInvalidate();
                        invalidateVirtualView(virtualViewId);
                        return true;
                    }
                    return false;
                }
                default:
                    return false;
            }
        }
    }
}
